package com.wyw.learning.thread.threadlocal;

/**
 * 演示ThreadLocal用法2：避免参数传递的麻烦
 *  比如service方法调用其他service方法，但是用户信息都需要一直传下去，这里可以改成使用UserMap
 *  利用共享Map的问题：可以用static的ConcurrentHashMap，把当前线程的ID作为key，把user作为value来保存，这样可以做到线程间的隔离，但是依然有性能影响。
 *      多个线程同时操作这个Map的时候也会有一定的同步时间或者CAS时间。
 *
 * ThreadLocal的两个作用
 *  1、某个对象是线程隔离的（因为每个线程都有自己独立的对象）
 *  2、在任何方法中都可以轻松获取该对象
 *
 * initialValue与set的选择（根据共享对象生成的时机不同，进行选择）
 *  场景一：initialValue     在ThreadLocal第一次get的时候把对象初始化出来，对象的初始化可以由我们控制
 *  场景二：set     如果需要保存到ThreadLocal里的对象生成时机不由我们随意控制（例如拦截器中生成用户信息，使用set，方便后续的使用）
 *  同：两种场景最后都是通过set方法直接设置值
 *  异：起点和入口不一样
 *
 * 使用ThreadLocal带来的四点好处
 *  1、达到线程安全
 *  2、不需要加锁，提高执行效率
 *  3、高效地利用内存、节省开销：相比与每个任务都新建一个对象
 *  4、避免了传参的麻烦：使代码耦合度更低、更优雅
 *
 * ThreadLocal重要方法
 *  T initialValue():初始化
 *      1、该方法返回当前线程对应的"初始值"，这是一个延迟加载的方法，只有调用get的时候，才会触发
 *      2、当线程第一次使用get方法访问变量时，将调用此方法，除非当前线程先调用了set方法，在这种情况下，不会为线程调用本initialValue方法
 *          对应了ThreadLocal的两种典型用法
 *      3、每个线程最多调用一次此方法，但如果已经调用了remove方法，在调用get方法时，则可以再次调用此方法
 *      如果不重写本方法，这个方法就会返回null，一般使用匿名内部类的方法重写initialValyue方法，以便在后续使用中可以初始化副本对象
 *  void set（T t）：为这个线程设置一个新值
 *  T get（）：得到这个线程对应的value，如果是首次调用get方法，则会调用initialize来得到这个值
 *  void remove（）：删除这个线程对应的值
 *
 * ThreadLocalMap类是每个线程Thread类里面的变量，里面最重要的是一个键值对数组Entry[] table，可以按照hashMap去理解简单结构
 *  键：这个ThreadLocal
 *  值：实际需要的成员变量，比如user或者simpleDateFormat对象
 *  与HashMap在处理hash冲突的时候略有不同
 *      hashMap：如果发生冲突就用链表往下，Java 8之后，当链表达到一定长度之后，就使用红黑树的方式
 *      threadLocalMap使用的是线性探测法，当发生冲突，就继续查找下一个空位置
 *
 * 使用ThreadLocal注意点：
 *  1、内存泄露
 *      什么是内存泄露：某个对象不再有用，但是占用的内存却不能被回收
 *      key的泄露与value的泄露：threadLocalMap的entry继承自WeakReference，是若引用（弱引用的特点：如果这个对象只被弱引用关联，没有任何强引用关联，那么这个对象就可以被回收）value是强引用（如果设计成弱引用，那么可能在需要value的时候却提示已经被回收了）
 *      正常情况下：当线程终止，保存在ThreadLocal里的value就会被回收，因为此时已经没有任何强引用了
 *      但是，如果线程始终不终止（比如线程需要保持很久，比如在使用线程池的时候，实际上都是使用同一个线程），那么key对应的value就不能回收，因为有之下的调用链：
 *          Thread ————> ThreadLocalMap —————> Entry（key为null）  —————>   Value
 *          因为value和Thread之间还存在这个强引用链路，所以导致value无法回收，这样就可能会出现OOM
 *          解决方案：JDK在set，remove，rehash方法中会扫描key为null的Entry，并把对应的value设置为null，这样value对象就可以被回收
 *          特殊情况：如果一个ThreadLocal不被使用，那么实际上set，remove，rehash方法也不会被调用，如果同时线程又不停止，那么调用链就一直存在，那么就导致了value的内存泄露
 *          如何避免（阿里规约）：调用remove方法，就会删除对应的Entry对象，可以避免内存泄露问题，所以当使用完ThreadLocal之后，应该调用remove方法
 *  2、空指针问题
 *      详见ThreadLocalNPE类
 *  3、共享对象
 *      如果在每个线程中ThreadLocal.set()写入的本来就是多线程共享的同一个对象，比如static对象，那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身，还是有并发访问的问题
 *  4、能不使用ThreadLocal就不要使用（比如任务数很少的情况）
 *  5、优先使用框架的支持，而不是自己造轮子
 *      例如在Spring中，如果可以使用RequestContextHolder，那么就不需自己维护ThreadLocal（避免忘记remove导致内存泄露）
 *      比如DateTimeContextHolder
 *      RequestContextHolder
 *
 *      每个Http请求都对应一个线程，每个线程之间是相互隔离的，这就是ThreadLocal的典型应用场景。
 *
 *
 * @author Mr Wu    yewen.wu.china@gmail.com
 * @date 2021/7/11   10:56 上午
 * <p>
 * Update History:
 * Author        Time            Content
 */
public class ThreadLocalNormalUsage06 {

    public static void main(String[] args) {
        new Service1().process();
    }
}

/**
 * 用户信息持有者
 */
class UserContextHolder{
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class Service1{
    public void process(){
        User user = new User("张三");
//        ThreadSafeFormatter.dateFormatThreadLocal.get();//像这样就是用了多个ThreadLocal对象
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2{
    public void process(){
        User user1 = UserContextHolder.holder.get();
        System.out.println("service2:"+user1.name);
        new Service3().process();
    }
}

class Service3{
    public void process(){
        User user1 = UserContextHolder.holder.get();
        System.out.println("service3:"+user1.name);
        UserContextHolder.holder.remove();//应该在最后的调用方法，主动的进行回收，如果使用的拦截器的方法，那么也应该在拦截器调用remove方法
    }
}


class User{
    String name;

    public User(String name) {
        this.name = name;
    }
}

